/*******************************************************************************
 * Copyright 2010 Prometheus Consulting
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package nz.co.senanque.madura.bundle;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.util.StringUtils;

/**
 * This bundle manager is intended for demo purposes. It accepts a fixed list of URLs that point at jar files
 * and it loads those, and only those, bundles. So bundles cannot be switched in and out (which is normally the whole point)
 * However it does allow us to deploy demos to CloudFoundry and similar environments which cannot support a local file system.
 * 
 * TODO: this could be enhanced to have a way to dynamically add to the URL list using some kind of admin interface (or possibly JMX) 
 * 
 * @author Roger Parkinson
 * @version $Revision: 1.5 $
 */
public class FixedUrlsBundleManagerImpl implements BundleManager, InitializingBean, BeanFactoryAware
{
    private Logger m_logger = LoggerFactory.getLogger(this.getClass());
    private Map<String,BundleRoot> m_bundleMap = new HashMap<String,BundleRoot>();
    private ThreadLocal<String> m_currentBundle = new ThreadLocal<String>();
    private boolean m_lock = false;
    private Map<String,Object> m_inheritableBeans = new HashMap<String,Object>();
    private DefaultListableBeanFactory m_beanFactory;
    private String m_export;
    private Set<BundleListener> m_bundleListeners = new HashSet<BundleListener>();
    private boolean m_childFirst = true;
    private String m_defaultBundle;
    private List<URL> m_urls = new ArrayList<URL>();
    
    public void init()
    {
        if (m_export != null)
        {
            for (String beanName: StringUtils.commaDelimitedListToSet(m_export))
            {
               m_inheritableBeans.put(beanName,m_beanFactory.getBean(beanName));
            }
        }
        for (BundleListener bundleListener:m_beanFactory.getBeansOfType(BundleListener.class).values())
        {
            m_bundleListeners.add(bundleListener);
        }
        try {
			scan();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
    }
    public synchronized void scan() throws IOException
    {
        m_logger.debug("Scanning files");
        JarInputStream[] jarInputStream = new JarInputStream[getUrls().size()];
        int i = 0;
        for (URL url: getUrls())
        {
        	String urlString = url.getPath();
        	int j = urlString.lastIndexOf('/');
        	int j1 = urlString.lastIndexOf('-');
        	String bundleName = urlString.substring(j+1,j1);
        	InputStream is = url.openStream();
        	addBundle(bundleName,is,0);
        	is.close();
        }
    }
    @SuppressWarnings("unchecked")
    private void addBundle(String bundleName, InputStream inputStream, long lastModified)
    {
        try
        {
            JarInputStream jarInputStream = new JarInputStream(inputStream);
            Manifest mf = jarInputStream.getManifest();
            Attributes attributes = mf.getMainAttributes();
            Properties properties = new Properties();
            for (Map.Entry<Object,Object> a :attributes.entrySet())
            {
                Object key = a.getKey();
                Object value = a.getValue();
                properties.setProperty(key.toString(), value.toString());
                m_logger.debug("Property: {} value {}",key.toString(), value.toString());
            }
            properties.setProperty("bundle.name", 
                    figureBundleName(
                            String.valueOf(properties.get("Bundle-Name")),
                            String.valueOf(properties.get("Bundle-Version"))));
            properties.setProperty("bundle.file", properties.getProperty("bundle.name"));
            String className = attributes.getValue("Bundle-Activator");
            String classPath = attributes.getValue("Class-Path");
            List<URL> urls = new ArrayList<URL>();
            if (classPath != null && classPath.length() > 0)
            {
                StringTokenizer st = new StringTokenizer(classPath," ");
                while (st.hasMoreTokens())
                {
                    String pathElement = st.nextToken();
                    File f = new File(pathElement);
                    if (!f.canRead())
                    {
                        throw new RuntimeException("Cannot read file: "+pathElement);
                    }
                    urls.add(f.toURI().toURL());
                }
            }
            URL[] urlArray = urls.toArray(new URL[]{});
            ClassLoader cl = this.getClass().getClassLoader();
            ClassLoader classLoader = new BundleClassLoader(m_childFirst, urlArray, new JarInputStream[]{jarInputStream}, cl);
            Class<BundleRoot> clazz = (Class<BundleRoot>)classLoader.loadClass(className);
            Class<?> clazz1 = classLoader.loadClass("nz.co.senanque.madura.bundle.InnerBundleFactory");
            ClassLoader actual = clazz.getClassLoader();
            BundleRoot root = (BundleRoot)clazz.newInstance();
            root.setDate(lastModified);
            root.init(m_beanFactory,properties,classLoader,m_inheritableBeans);
            m_bundleMap.put(bundleName, root);
            m_defaultBundle = bundleName;
            for (BundleListener bundleListener: m_bundleListeners)
            {
                bundleListener.add(bundleName,root);
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
    public void shutdown()
    {
        
    }
    public BundleRoot getBundle()
    {
        // Fairly crude locking mechanism to ensure that the 
        // the method waits if the bundle list is being worked on
        // We don't want to take out a lock here because there's no need to lock out other getters
        while (m_lock)
        {
            try
            {
                Thread.sleep(500);
            }
            catch (InterruptedException e)
            {
            }
        }
        String bundleName = m_currentBundle.get();
        if (bundleName == null)
        {
            bundleName = m_defaultBundle;
        }
        if (bundleName == null)
        {
            throw new RuntimeException("No bundle selected and default bundle is not set");
        }
        return m_bundleMap.get(bundleName);
    }
    public void setBundle(String bundleName, String version)
    {
        m_currentBundle.set(selectBestBundle(bundleName,version));
    }
    public void setBundle(String bundleName)
    {
        m_currentBundle.set(selectBestBundle(bundleName,null));
    }
    private String selectBestBundle(String bundle, String version)
    {
        bundle = figureBundleName(bundle,version);
        String previousBundle = m_bundleMap.get(bundle) != null?bundle:null;
        if (previousBundle != null)
            return previousBundle;
        for (String key : m_bundleMap.keySet())
        {
            if (bundle.compareTo(key)>0)
                return previousBundle;
            previousBundle = key;
        }
        if (previousBundle == null)
            throw new RuntimeException("No bundle found for "+bundle);
        return previousBundle;
    }
    public Map<?,BundleRoot> getBeansOfType(Class<?> clazz)
    {
        Map<Object,BundleRoot> ret = new HashMap<Object,BundleRoot>();
        for (Map.Entry<String, BundleRoot> entry: m_bundleMap.entrySet())
        {
            for (Object o: entry.getValue().getApplicationContext().getBeansOfType(clazz).values())
            {
                ret.put(o,entry.getValue());
            }
        }
        return ret;
    }
    public Collection<BundleRoot> getAvailableBundles()
    {
        return Collections.unmodifiableCollection(m_bundleMap.values());
    }
    private String figureBundleName(String bundle, String version)
    {
        if (version != null)
            bundle += "-"+version;
        return bundle;
    }
    public void afterPropertiesSet()
    {
        init();
    }
    public Map<String, Object> getInheritableBeans()
    {
        return m_inheritableBeans;
    }
    public void setInheritableBeans(Map<String, Object> inheritableBeans)
    {
        m_inheritableBeans = inheritableBeans;
    }
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException
    {
        m_beanFactory = (DefaultListableBeanFactory)beanFactory;
    }
    public String getExport()
    {
        return m_export;
    }
    public void setExport(String export)
    {
        m_export = export;
    }
    public boolean isChildFirst()
    {
        return m_childFirst;
    }
    public void setChildFirst(boolean childFirst)
    {
        m_childFirst = childFirst;
    }
    @Required
	public List<URL> getUrls() {
		return m_urls;
	}
	public void setUrls(List<URL> urls) {
		m_urls = urls;
	}
}
